package com.t.bricks.bussiness.controller.recharge;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;

import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import com.t.bricks.bussiness.db.entity.recharge.Recharge;
import com.t.bricks.bussiness.db.entity.recharge.base.WechatRefundBase;
import com.t.bricks.bussiness.service.recharge.WeChatPayService;
import com.t.bricks.bussiness.util.recharge.WXPayConfigCustom;
import com.t.bricks.model.Log;
import com.t.bricks.model.MsgEmity;
import com.t.bricks.utils.function.data.MoneyUtil;
import com.t.bricks.utils.function.data.UUIDUtil;
import com.t.bricks.utils.function.encode.MD5Util;
import com.t.bricks.utils.function.json.JacksonShell;

/**
 * 参考: https://blog.csdn.net/weixin_45865428/article/details/117807648
 * @author huangxinbian
 * 
 * 步骤:
 * 1.在公众号设置"业务域名"\"JS接口安全域名"\"网页授权域名" 如 "04demo.tomatomeatman.icu"
 * 2.在公众号设置"白名单",把域名指向的IP加入,如"04demo.tomatomeatman.icu"域名为"42.192.8.108"
 * 3.做个二维码,访问地址为: "http://04demo.tomatomeatman.icu/getWXOAuth2Code"
 * 4.用微信扫二维码,进入后点击支付
 * 
 * 逻辑:
 * 1.微信扫码访问http://xxx.icu/pay/wx/getWXOAuth2Code获得code,并设置跳转页,跳转页为 http://xxx.icu/pay/wx/wx-oauth-code-return
 * 2.自动跳转到http://xxx.icu/pay/wx/wx-oauth-code-return时得到openId, 然后跳转到支付业务页面,如http://xxx.icu/pay/wx/confirm?openid=" + openid
 * 3.进入/pay/wx/confirm页面后通过openId和付费信息组合成订单(如订单号"A123")提交到微信, 并建立签名
 * 4.得到签名后返回一个支付情况页面如:http://xxx.icu/wxpay,目的是让客户看到待支付的内容
 * 5.客户点击"支付"按键自动进入微信的支付密码录入,完成支付
 * 6.支付完成后跳转到查询订单支付情况查询页面按订单号查询订单结果(如按订单号"A123"查询),注意:跳转由http://xxx.icu/wxpay页面中的 js代码window.location.href 定义
 */
@Controller
@Api(value="微信支付", tags={"微信支付操作接口"})
@RequestMapping("/pay/wx/")
public class WxPayController {

	/**
	 * 微信公众号的appid,同时设置在微信平台
	 */
	@Value("${app.wechat.appid:}")
	private String appId;
	
	/**
	 * 微信公众号的appsecret,同时设置在微信平台
	 */
	@Value("${app.wechat.appsecret:}")
	private String appSecret;
	
	/**
	 * 微信商户ID,同时设置在微信平台
	 */
	@Value("${app.wechat.mch.id:}")
	private String mchId;

	/**
	 * 微信商户API密钥,同时设置在微信平台
	 */
	@Value("${app.wechat.mch.api:}")
	private String mchKey;

	/**
	 * 微信支付的IP白名单,同时设置在微信平台
	 */
	@Value("${app.wechat.white.ip:}")
	private String whiteIP;

	/**
	 * 本程序的对外域名,同时设置在微信平台白名单
	 */
	@Value("${app.wechat.official.account.domain:}")
	private String payDomain;

	/**
	 * 内部访问密钥,注意发布时必须修改
	 */
	@Value("${app.key:}")
	private String appKey;
	
	@Autowired
	private WeChatPayService weChatPayService;
	
	/**
	 * 支付入口,先从获取授权码开始
	 * @param sOtherParam 其它要向下传递的参数
	 * @return
	 * @throws Exception 
	 */
	@ApiOperation(value="支付入口", notes="支付入口,先从获取授权码开始")
	@RequestMapping(method={RequestMethod.GET}, value="/getWXOAuth2Code")
	public String getWXOAuth2Code(HttpServletRequest request, HttpServletResponse response) throws Exception {
		String sOtherParam = request.getParameter("pr");
		
		String redirectUri = URLEncoder.encode(
				String.format(
					"http://%s/pay/wx/wx-oauth-code-return?sOtherParam=%s",//"/pay/wx"是第一轮回调地址前缀
					payDomain,//本程序的对外域名,同时设置在微信平台白名单
					sOtherParam),
				"UTF-8");
		
		String url = String.format(
				"redirect:https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect",
				appId,
				redirectUri
		);
		
		return url;
	}

	/**
	 * 第一轮回调'授权码回调'，传入授权码和state，/wx-oauth-code-return?code=授权码&state=
	 * @param code 授权码
	 * @param state 申请授权码传入微信的值，被原样返回
	 * @param sOtherParam 其它(从入口引入的)参数
	 * @return
	 */
	@ApiOperation(value="授权码回调", notes="第一轮回调'授权码回调'，传入授权码和state，/wx-oauth-code-return?code=授权码&state=")
	@RequestMapping(method={RequestMethod.GET}, value="/wx-oauth-code-return")
	public String wxOAuth2CodeReturn(@RequestParam String code,
			@RequestParam String state, @RequestParam String sOtherParam) {
//		if (null == sOtherParam || "".equals(sOtherParam.trim()) || "null".equalsIgnoreCase(sOtherParam.trim())) {
//			return String.format("redirect:http://%s/pay/wx/error?msg=%s", payDomain, "获取通道号失败");
//		}
		
		String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
				appId,//微信公众号的appid,同时设置在微信平台
				appSecret,//微信公众号的appsecret,同时设置在微信平台
				code//上一个链接获得的代码值,由腾讯赋予
		);

		//申请openid，请求url
		RestTemplate restTemplate = new RestTemplate();
		ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
		
		//申请openid接口响应的内容，其中包括了openid
		String body = exchange.getBody();
		Log.debug("申请openid响应的内容:", body);
		Log.debug("其它(从入口引入的)参数:", sOtherParam);
		
		Map<String, Object> map = JacksonShell.toJavaObject(body, Map.class);
		if (null == map) {
			return String.format("redirect:http://%s/pay/wx/error?msg=%s", payDomain, "获取授权码错误");
		}
		
		String openid = String.valueOf(map.get("openid"));//获取openid
		
		//return "redirect:" + wxOAuth2CodeReturnUrl + "?openid=" + openid;//这个地址需要在支付设置中加入,否则会在最终付款时提示错误
		try {
			sOtherParam = URLEncoder.encode(sOtherParam, "UTF-8");
		} catch (Exception e) {
			Log.error("URL编码异常:", e.getMessage());
		}
		
		return String.format("redirect:http://%s/pay/wx/confirm?openid=%s&sOtherParam=%s", payDomain, openid, sOtherParam);
	}
	
	//---------------------------- 小程序 开始 ---------------------------------//
	
	/**
	 * 小程序请求openId
	 * @param code
	 * @return
	 */
	@ApiOperation(value="小程序请求openId", notes="接收openid")
	@ApiImplicitParams({
		@ApiImplicitParam(name="code", value="请求凭证,由腾讯赋予5分钟有效", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/openid")
	@ResponseBody
	public MsgEmity smallRoutineOpenId(String code) {
		String url = String.format("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&grant_type=authorization_code&js_code=%s",
				appId,//微信公众号的appid,同时设置在微信平台
				appSecret,//微信公众号的appsecret,同时设置在微信平台
				code//请求凭证,由腾讯赋予5分钟有效
		);

		//申请openid，请求url
		RestTemplate restTemplate = new RestTemplate();
		ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
		
		//{"errcode":40029,"errmsg":"invalid code, rid: 632d6bc4-4393ec07-78c7e598"}
		//{"session_key":"o7zHuHaNrtoAnuLn2HXEYA==","openid":"o3zai5Xy6Ud_tj5_6Gjea5O3x1F4","unionid":"ogHdAsyghDByo1ffFqmKAMTCLs-o"}
		String body = exchange.getBody();
		Map<String, Object> map = JacksonShell.toJavaObject(body, Map.class);
		if (null == map) {
			return MsgEmity.err(1001, "请求失败");
		}
		
		if (map.containsKey("errcode")) {
			return new MsgEmity(false, "获取验证码失败", map);
		}
		
		if (!map.containsKey("openid")) {
			Log.error("小程序请求openId返回:", body);
			return MsgEmity.err(1003, "返回结果与预定结构不符");
		}
		
		return MsgEmity.success(map.get("openid"), "获取成功");
	}
	
	/**
	 * 小程序统一下单
	 * @param sOpenid 微信公开号
	 * @param sFund 充值金额
	 * @return
	 */
	@ApiOperation(value="小程序统一下单", notes="代理请求访问")
	@ApiImplicitParams({
		@ApiImplicitParam(name="sOpenid", value="微信公开号", dataType="String", paramType="query", required=true),
		@ApiImplicitParam(name="sFund", value="充值金额", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/confirm")
	@ResponseBody
	public MsgEmity smallRoutineCreateOrder(String sOpenid, String sFund) {
		if (null == sOpenid || "".equals(sOpenid.trim())) {
			return MsgEmity.err(9001, "未能获得微信用户公共号");//没有获得微信用户公共号
		}
		
		if (null == sFund || "".equals(sFund.trim())) {
			return MsgEmity.err(9002, "未能获得充值金额");//没有获得充值金额
		}
		
		if (!MoneyUtil.isBigDecimal(sFund)) {
			return MsgEmity.err(9003, "获取的充值金额格式错误:", sFund);//没有获得充值金额
		}
		
		BigDecimal mFund = MoneyUtil.string2BigDecimal(sFund);
		if (!MoneyUtil.isPlus(mFund)) {
			return MsgEmity.err(9004, "充值金额必须是正数");//充值金额为负数
		}
		
		MsgEmity me = weChatPayService.createOrder(sOpenid, mFund);
		if (!me.isSuccess()) {
			return me;
		}
		
		Recharge recharge = me.getData();
		
		String totalFee = MoneyUtil.toCent(recharge.getmFund());//元转分
		
		String sign = new StringBuilder(400)
				.append("appid=").append(appId)
				.append("&attach=微信充值订单").append(recharge.getsOrderNo())//
				.append("&body=").append(recharge.getsBody())
				//.append("&device_info=").append()//设备号,自定义参数，可以为请求支付的终端设备号等,非必填
				.append("&detail=订单详情").append(recharge.getsBody())//
				.append("&mch_id=").append(mchId)
				.append("&nonce_str=").append(recharge.getsOrderNo())
				.append("&notify_url=none")//
				.append("&openid=").append(recharge.getsOpenid())
				.append("&out_trade_no=").append(recharge.getsOrderNo())
				.append("&spbill_create_ip=").append(recharge.getsSpbillCreateIp())
				.append("&total_fee=").append(totalFee)
				.append("&trade_type=JSAPI")
				.append("&key=").append(mchKey)//放最后
				.toString();
		sign = MD5Util.MD5(sign);
		
		//以xml结构发送时
		String paramXml = new StringBuilder(1000)
			.append("<xml>")
			.append("	<appid>").append(appId).append("</appid>")
			.append("	<attach>微信充值订单").append(recharge.getsOrderNo()).append("</attach>")
			.append("	<body>").append(recharge.getsBody()).append("</body>")
			.append("	<mch_id>").append(mchId).append("</mch_id>")
			.append("	<detail>订单详情").append(recharge.getsBody()).append("</detail>")
			.append("	<nonce_str>").append(recharge.getsOrderNo()).append("</nonce_str>")
			.append("	<notify_url>none</notify_url>")
			.append("	<openid>").append(recharge.getsOpenid()).append("</openid>")
			.append("	<out_trade_no>").append(recharge.getsOrderNo()).append("</out_trade_no>")
			.append("	<spbill_create_ip>").append(recharge.getsSpbillCreateIp()).append("</spbill_create_ip>")
			.append("	<total_fee>").append(totalFee).append("</total_fee>")
			.append("	<trade_type>JSAPI</trade_type>")
			.append("	<sign>").append(sign).append("</sign>")
			.append("</xml>")
			.toString();
		
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_XML);//参数以xml进行发送
		
		RestTemplate restTemplate = new RestTemplate();
		
		//以UTF-8作为编码接收
		List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();
		httpMessageConverters.stream().forEach(httpMessageConverter -> {
			if(httpMessageConverter instanceof StringHttpMessageConverter){
				StringHttpMessageConverter messageConverter = (StringHttpMessageConverter) httpMessageConverter;
				messageConverter.setDefaultCharset(Charset.forName("UTF-8"));
			}
		});
		
		//HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(paramMap, headers);//以json结构发送时
		HttpEntity<String> httpEntity = new HttpEntity<>(paramXml, headers);
		ResponseEntity<String> exchange = restTemplate.postForEntity("https://api.mch.weixin.qq.com/pay/unifiedorder", httpEntity, String.class);
		
		String body = exchange.getBody();
		
//		Log.debug("小程序统一下单响应的内容:", body);
		if (body.contains("<return_code><![CDATA[FAIL]]></return_code>")) {
			return MsgEmity.err(9007, "统一下单失败,返回内容:", body);
		}
		
		if (!body.contains("<return_code><![CDATA[SUCCESS]]></return_code>")) {
			return MsgEmity.err(9008, "统一下单失败,未能获取预定格式,返回内容:", body);
		}
		
		return MsgEmity.success((Object)recharge.getsOrderNo(), "统一下单成功,返回内容:", body);
	}
	
	/**
	 * 小程序统一下单二次签名
	 * @param nonceStr
	 * @param prepayId
	 * @return
	 */
	@ApiOperation(value="小程序二次签名", notes="代理请求访问")
	@ApiImplicitParams({
		@ApiImplicitParam(name="nonceStr", value="订单号", dataType="String", paramType="query", required=true),
		@ApiImplicitParam(name="prepayId", value="第一次签名", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/paysign")
	@ResponseBody
	public MsgEmity smallRoutineCreateSign(String nonceStr, String prepayId) {
		long time = System.currentTimeMillis();
		String sign = new StringBuilder(192)
				.append("appId=").append(appId)
				.append("&nonceStr=").append(nonceStr)
				.append("&package=prepay_id=").append(prepayId)
				.append("&signType=MD5")
				.append("&timeStamp=").append(time)
				.append("&key=").append(mchKey)//放最后
				.toString();
		sign = MD5Util.MD5(sign);
		
		return new MsgEmity(true, sign, time);
	}
	
	/**
	 * 小程序退款
	 * @param sOrderNo 充值订单号
	 * @param sRechargeId 购物编号
	 * @param sWaresId 商品编号
	 * @param sMoney 待退金额
	 * @param sMemo 备注
	 * @return
	 */
	@ApiOperation(value="小程序退款", notes="限制-仅允许后台操作")
	@ApiImplicitParams({
		@ApiImplicitParam(name="sOrderNo", value="充值订单号", dataType="String", paramType="query", required=true),
		@ApiImplicitParam(name="sRechargeId", value="购物编号", dataType="String", paramType="query", required=true),
		@ApiImplicitParam(name="sWaresId", value="商品编号", dataType="String", paramType="query", required=false),
		@ApiImplicitParam(name="sMoney", value="待退金额", dataType="String", paramType="query", required=true),
		@ApiImplicitParam(name="sMemo", value="备注", dataType="String", paramType="query", required=false)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/refund")
	@ResponseBody
	public MsgEmity smallRoutineRefund(String sOrderNo, String sRechargeId,
			String sWaresId, String sMoney, String sMemo) {
		if (null == sOrderNo || "".equals(sOrderNo.trim())) {
			return MsgEmity.err(9001, "充值订单号为空"); 
		}
		
		sMoney = sMoney.trim();
		BigDecimal mMoney = MoneyUtil.toBigDecimal(sMoney);
		if (!MoneyUtil.isPlus(mMoney)) {
			return MsgEmity.err(9002, "待退金额必须是正数");
		}

		if (null == sRechargeId || "".equals(sRechargeId.trim())) {
			return MsgEmity.err(9003, "购物编号为空"); 
		}

		sOrderNo = sOrderNo.trim();
		sRechargeId = sRechargeId.trim();
		
		if (null == sWaresId || "".equals(sWaresId.trim())) {
			sWaresId = "ALL";
		}
		else {
			sWaresId = sWaresId.trim();
		}
			
		MsgEmity me = weChatPayService.findByOrder(sOrderNo);
		if (!me.isSuccess()) {
			return me;
		}
		
		Recharge recharge = me.getData();
		
		if (recharge.getiState() < 3 || recharge.getiState() > 4) {
			return MsgEmity.err(9004, "退款只限(3:已支付;4:已同步)");
		}
		
		if (recharge.getmFund().compareTo(mMoney) == -1) {
			return MsgEmity.err(9005, "退款金额超出原支付金额");
		}
		
		String outRefundNo = MD5Util.MD5(sOrderNo, ";", sRechargeId, ";", sWaresId);
		String refundFee = MoneyUtil.toCent(mMoney);
		String totalFee = MoneyUtil.toCent(recharge.getmFund());
		
		WXPayConfigCustom config = new WXPayConfigCustom(appId, mchId, mchKey);
		WXPay wxpay = new WXPay(config);
		
		Map<String, String> data = new HashMap<String, String>();
		data.put("nonce_str", UUIDUtil.get());
		data.put("out_refund_no", outRefundNo);
		data.put("out_trade_no", sOrderNo);
		data.put("refund_fee", refundFee);
		data.put("total_fee", totalFee);
		
		Map<String, String> resp = null;
		try {
			resp = wxpay.refund(data);
			wxpay.refund(resp);
		} catch (Exception e) {
			Log.error(e.getMessage());
			return MsgEmity.err(9006, "退款业务发生异常:", e.getMessage());
		}
		
		//失败: map = {nonce_str=ea78f4e47dd54c95be48a8a05e5f51d0, appid=wxa89cac8ca16a7a49, sign=95B3184AF9BDC01D65C20A758DD42A35, err_code=ORDERNOTEXIST, return_msg=OK, result_code=FAIL, err_code_des=订单不存在, mch_id=1619435221, return_code=SUCCESS, sign_type=MD5}
		if ("FAIL".equals(resp.get("return_code"))) {
			return MsgEmity.err(9007, "退款请求失败:", resp.get("err_code_des"));
		}
		
		//成功: {"transaction_id":"4200001559202210103404280552","nonce_str":"e2cdf28245e542199e7071349abf890d","out_refund_no":"920FFD1870415D6ED7FDE950B2ED320C","sign":"6062C542D9A24BF4C4B305293AB5A926","return_msg":"OK","mch_id":"1619435221","refund_id":"50302303432022101025770940729","cash_fee":"100","sOrderNo":"F61D2B35B14646E794AC950D2FFD901D","out_trade_no":"F61D2B35B14646E794AC950D2FFD901D","coupon_refund_fee":"0","refund_channel":"","appid":"wxa89cac8ca16a7a49","refund_fee":"2","total_fee":"100","result_code":"SUCCESS","coupon_refund_count":"0","cash_refund_fee":"2","return_code":"SUCCESS","sign_type":"MD5"}
		if (!"SUCCESS".equals(resp.get("return_code"))) {
			Log.info("退款请求未返回指定格式:", JacksonShell.toJSONString(resp));
			return MsgEmity.err(9007, "退款请求未返回指定格式");
		}
		
		if ("FAIL".equals(resp.get("result_code"))) {
			return MsgEmity.err(9008, "退款申请腾讯接收成功,但请求失败:", resp.get("err_code_des"));
		}
		
		if (!"SUCCESS".equals(resp.get("result_code"))) {
			return MsgEmity.err(9009, "退款申请腾讯接收成功,但请求失败:", resp.get("return_msg"));
		}
		
		me = weChatPayService.hasWechatRefund(outRefundNo);
		if (!me.isSuccess()) {
			return me;
		}
		
		int i = me.getData();
		if (i != 0) {
			return MsgEmity.err(9009, "退款已经通过");
		}
		
		WechatRefundBase wechatRefund = new WechatRefundBase();
		wechatRefund.setsId(outRefundNo);
		wechatRefund.setsRechargeId(sOrderNo);
		wechatRefund.setsOrderId(sRechargeId);
		wechatRefund.setsWaresId(sWaresId);
		wechatRefund.setiTotalFee(Integer.valueOf(totalFee));
		wechatRefund.setiRefundFee(Integer.valueOf(refundFee));
		wechatRefund.setsMemo(sMemo.trim());
		
		me = weChatPayService.addWechatRefund(wechatRefund);
		if (!me.isSuccess()) {
			Log.error("腾讯退款成功,但本程序退款记录保存失败");
			return MsgEmity.err(9010, "腾讯退款成功,但本程序退款记录保存失败");
		}

		return MsgEmity.success(wechatRefund, "退款成功");
	}
	
	/**
	 * 小程序查询退款情况
	 * @param sOrderNo 充值订单号
	 * @return
	 */
	@ApiOperation(value="小程序查询退款情况", notes="查询退款情况")
	@ApiImplicitParams({
		@ApiImplicitParam(name="sOrderNo", value="充值订单号", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/refund/look")
	@ResponseBody
	public MsgEmity smallRoutineRefund(String sOrderNo) {
		if (null == sOrderNo || "".equals(sOrderNo.trim())) {
			return MsgEmity.err(9001, "充值订单号为空"); 
		}
		
		WXPayConfigCustom config = new WXPayConfigCustom(appId, mchId, mchKey);
		WXPay wxpay = new WXPay(config);
		
		Map<String, String> data = new HashMap<String, String>();
		data.put("out_trade_no", sOrderNo);
		
		Map<String, String> resp = null;
		try {
			resp = wxpay.refundQuery(data);
			wxpay.refund(resp);
		} catch (Exception e) {
			Log.error(e.getMessage());
			return MsgEmity.err(9007, "查询退款业务发生异常:", e.getMessage());
		}
		
		resp.put(sOrderNo, sOrderNo);

		return MsgEmity.success(resp, "查询退款请求成功");
	}
	
	/**
	 * 小程序请求手机信息
	 * @param code
	 * @return
	 */
	@ApiOperation(value="小程序请求手机信息", notes="查询手机号")
	@ApiImplicitParams({
		@ApiImplicitParam(name="code", value="请求凭证,由腾讯赋予5分钟有效", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/phone")
	@ResponseBody
	public MsgEmity smallRoutinePhone(String code) {
		String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
				appId,//微信公众号的appid,同时设置在微信平台
				appSecret//微信公众号的appsecret,同时设置在微信平台
		);

		//获取小程序全局唯一后台接口调用凭据，请求url
		RestTemplate restTemplate = new RestTemplate();
		ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
		
		//{"access_token":"ACCESS_TOKEN","expires_in":7200}
		//{"errcode":40013,"errmsg":"invalid appid"}
		String body = exchange.getBody();
		Map<String, Object> map = JacksonShell.toJavaObject(body, Map.class);
		if (null == map) {
			return MsgEmity.err(1001, "请求失败");
		}
		
		if (map.containsKey("errcode")) {
			return new MsgEmity(false, "获取调用凭据失败", map);
		}
		
		if (!map.containsKey("access_token")) {
			Log.error("小程序请求access_token返回:", body);
			return MsgEmity.err(1002, "返回结果与预定结构不符");
		}
		
		String accessToken = map.get("access_token").toString();
		url = String.format("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s",
				accessToken//请求凭证,由腾讯赋予5分钟有效
		);
		
		//设置请求编码参数
        HttpHeaders headers = new HttpHeaders();
        MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
        headers.setContentType(type);
        headers.add("Accept", MediaType.APPLICATION_JSON.toString());

        HttpEntity<String> formEntity = new HttpEntity<String>(String.format("{\"code\":\"%s\"}", code), headers);
        body = restTemplate.postForEntity(url, formEntity, String.class).getBody();
        
        //{"errcode":0,"errmsg":"ok","phone_info":{"phoneNumber":"18587897000","purePhoneNumber":"18587897000","countryCode":"86","watermark":{"timestamp":1666921234,"appid":"wxaaaaaaaa"}}}
		//{"errcode":47001,"errmsg":"data format error hint: [keNESbMre-vppfSA] rid: 635b2e81-555727f5-391603a8"}
        body = body.replaceAll(String.format("\"%s\"", appId), "\"-\"");
		map = JacksonShell.toJavaObject(body, Map.class);
		if (null == map) {
			return MsgEmity.err(1003, "请求失败");
		}
		
		if (!map.get("errcode").equals(0)) {
			return new MsgEmity(false, "请求失败", map);
		}
		
		if (!map.get("errmsg").equals("ok")) {
			return new MsgEmity(false, "请求错误", map);
		}
		
		if (!map.containsKey("phone_info")) {
			return new MsgEmity(false, "获取格式与预期不符", map);
		}
		
		return MsgEmity.success(map.get("phone_info"), "获取成功");
	}
	
	/**
	 * 小程序请求微信用户唯一标识
	 * @param code
	 * @return
	 */
	@ApiOperation(value="小程序请求微信用户唯一标识", notes="查询微信用户唯一标识")
	@ApiImplicitParams({
		@ApiImplicitParam(name="code", value="请求凭证,由腾讯赋予5分钟有效", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST}, value="/small/unionid")
	@ResponseBody
	public MsgEmity smallGetUnionid(String code) {
		if (null == code || "".equals(code.trim())) {
			return MsgEmity.err(1001, "code参数为空");
		}
		
		String url = String.format("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
				appId,//微信公众号的appid,同时设置在微信平台
				appSecret,//微信公众号的appsecret,同时设置在微信平台
				code
		);

		RestTemplate restTemplate = new RestTemplate();
		ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
		
		//{"session_key": "1TuU3Txxxxf5pMZw==","openid": "o3zai5Xy6Ud_tj5_6vvvv3x1F4","unionid": "ogHdAsyghDByoxxxxqmKAMTCLs-o"}
		//{"errcode": 40163,"errmsg": "code been used, rid: 639c07d6-5db69484-249ea25f"}
		String body = exchange.getBody();
		Map<String, Object> map = JacksonShell.toJavaObject(body, Map.class);
		if (null == map) {
			return MsgEmity.err(1002, "请求失败");
		}
		
		if (map.containsKey("errcode")) {
			return new MsgEmity(false, "获取unionid失败", map);
		}
		
		if (!map.containsKey("unionid")) {
			Log.error("小程序请求unionid返回:", body);
			return MsgEmity.err(1003, "返回结果与预定结构不符");
		}
		
		if (!map.containsKey("unionid")) {
			Log.error("小程序请求unionid返回:", body);
			return MsgEmity.err(1003, "返回结果与预定结构不符");
		}
		
		return MsgEmity.success(map.get("unionid"), "获取unionid成功");
	}
	
	//---------------------------- 小程序 结束 ---------------------------------//
	
	/**
	 * 统一下单，接收openid
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception
	 */
	@ApiOperation(value="统一下单", notes="统一下单，接收openid")
	@RequestMapping(method={RequestMethod.POST, RequestMethod.GET}, value="/confirm")
	@ResponseBody
	public MsgEmity confirm(HttpServletRequest request, HttpServletResponse response) throws Exception {
		String sOpenid = request.getParameter("openid");//从请求中获取openid
		if (null == sOpenid || "".equals(sOpenid.trim())) {
			return MsgEmity.err(9001, "未能获得微信用户公共号");//没有获得微信用户公共号
		}
		
		String sFund = request.getParameter("mFund");//充值金额
		if (null == sFund || "".equals(sFund.trim())) {
			return MsgEmity.err(9002, "未能获得充值金额");//没有获得充值金额
		}
		
		if (!MoneyUtil.isBigDecimal(sFund)) {
			return MsgEmity.err(9003, "获取的充值金额格式错误:", sFund);//没有获得充值金额
		}
		
		BigDecimal mFund = MoneyUtil.string2BigDecimal(sFund);
		if (!MoneyUtil.isPlus(mFund)) {
			return MsgEmity.err(9004, "充值金额必须是正数");//充值金额为负数
		}
		
		MsgEmity me = weChatPayService.createOrder(sOpenid, mFund);
		if (!me.isSuccess()) {
			return me;
		}
		
		Recharge recharge = me.getData();
		
		String totalFee = MoneyUtil.toCent(recharge.getmFund());
		
		Map<String, String> requestParam = new HashMap<>();//构造请求的参数
		requestParam.put("appid", appId);
		requestParam.put("mchid", mchId);
		requestParam.put("out_trade_no", recharge.getsOrderNo());//订单号[6,32],只能是数字、大小写字母_-*且在同一个商户号下唯
		requestParam.put("body", recharge.getsBody());//订单描述
		requestParam.put("fee_type", "CNY");//人民币
		requestParam.put("total_fee", totalFee); //金额
		
		//requestParam.put("spbill_create_ip", getIpAddr(request));//客户端ip//不能用这个,会报没有total_fee参数
		//requestParam.put("spbill_create_ip", "127.0.0.1");//客户端ip
		//requestParam.put("spbill_create_ip", "42.192.8.108");//客户端ip
		requestParam.put("spbill_create_ip", recharge.getsSpbillCreateIp());//客户端ip "182.88.141.181"
		//requestParam.put("spbill_create_ip", "192.168.1.4");//客户端ip
		
		requestParam.put("notify_url", "none");//微信异步通知支付结果接口，暂时不用
		requestParam.put("trade_type", "JSAPI");
		requestParam.put("openid", recharge.getsOpenid());

		//调用统一下单接口
		Map<String, String> resp = null;
		try {
			WXPay wxPay = new WXPay(new WXPayConfigCustom(appId, mchId, mchKey));//创建sdk客户端
			resp = wxPay.unifiedOrder(requestParam);
		} catch (Exception e) {
			resp = new HashMap<>();
			Log.error("统一下单操作异常");
		}

		//--准备h5页面需要的数据, 警告,添加jsapiPayParam的键不能随意调换顺序--//
		Map<String, String> jsapiPayParam = new HashMap<>(9);
		jsapiPayParam.put("appId", appId);
		jsapiPayParam.put("timeStamp", String.valueOf(System.currentTimeMillis()/1000));
		jsapiPayParam.put("nonceStr", UUID.randomUUID().toString());//随机字符串
		jsapiPayParam.put("package", String.format("prepay_id=%s", resp.get("prepay_id")));
		jsapiPayParam.put("signType", "MD5");//jsapiPayParam.put("signType","HMAC-SHA256");
		
		try {
			//WXPayUtil.generateSignature(jsapiPayParam,key,WXPayConstants.SignType.HMACSHA256));
			String sign = WXPayUtil.generateSignature(jsapiPayParam, mchKey, WXPayConstants.SignType.MD5);
			jsapiPayParam.put("paySign", sign);//建立签名
		} catch (Exception e) {
			Log.error("获取签名异常:{}", e.getMessage());
			return MsgEmity.err(9006, "获取签名异常");//获取签名异常
		}
		
		jsapiPayParam.put("mchid", mchId);
		jsapiPayParam.put("total_fee", totalFee);
		jsapiPayParam.put("sOrderNo", recharge.getsOrderNo());
		jsapiPayParam.put("iTotalFee", MoneyUtil.toStr(recharge.getmFund(), 2));
	
		return new MsgEmity(true, "构造成功,等待提交", jsapiPayParam);
	}
	
	/**
	 * 支付成功通知
	 * @param sOrderNo 充值订单号
	 * @return
	 */
	@ApiOperation(value="支付成功通知", notes="验证微信订单")
	@ApiImplicitParams({
		@ApiImplicitParam(name="sOrderNo", value="充值订单号", dataType="String", paramType="query", required=true)
	})
	@RequestMapping(method={RequestMethod.POST, RequestMethod.GET}, value="/success")
	@ResponseBody
	public MsgEmity paySuccess(String sOrderNo) {
		MsgEmity me = validOrder(sOrderNo);
		if (!me.isSuccess()) {
			return me;
		}

		return weChatPayService.changeState(3, sOrderNo);//返回订单
	}
	
//	/**
//	 * 支付失败的跳转页面
//	 * @param sOrderNo
//	 * @return
//	 */
//	@RequestMapping(method={RequestMethod.POST, RequestMethod.GET}, value="/fail")
//	@ResponseBody
//	public String payFail(String sOrderNo) {
//		MsgEmity me = validOrder(sOrderNo);
//		if (me.isSuccess()) {//查看订单发现居然是正确的
//			Map<String, String> map = me.getData();
//			return getHtml("PayOrder", map);
//		}
//		
//		weChatPayService.changeState(2, sOrderNo);
//
//		return getHtml("PayError", "msg", me.getMsg());
//	}
	
//	/**
//	 * 错误信息页面
//	 * @param msg
//	 * @return
//	 */
//	@RequestMapping(method={RequestMethod.GET}, value="/error")
//	@ResponseBody
//	public String payError(String msg) {
//		return getHtml("PayError", "msg", msg);
//	}
	
	/**
	 * 查询订单
	 * @param orderNo
	 * @return
	 */
	@ApiOperation(value="查询订单", notes="查询微信订单")
	@RequestMapping(method={RequestMethod.GET}, value="/find/order")
	@ResponseBody
	public MsgEmity orderQuery(String sOrderNo) {
		return validOrder(sOrderNo);
	}
	
	/**
	 * 开发测试
	 * @param key 请求密钥
	 * @return
	 */
	@ApiOperation(value="开发测试", notes="必须输入访问密钥")
	@RequestMapping(method={RequestMethod.GET}, value="/test")
	@ResponseBody
	public MsgEmity test(String key) {
		if (null == key || "".equals(key) || !appKey.equals(key)) {
			return MsgEmity.err(9001, "密钥错误");
		}
		
		MsgEmity me = weChatPayService.createOrder("openid123", MoneyUtil.toBigDecimal("0.01"));
		if (!me.isSuccess()) {
			return me;
		}
		
		Recharge recharge = me.getData();

		return weChatPayService.changeState(3, recharge.getsOrderNo());
	}
	
	/**
	 * 验证订单
	 * @param sOrderNo
	 * @return
	 */
	private MsgEmity validOrder(String sOrderNo) {
		if (null == sOrderNo || "".equals(sOrderNo.trim())) {
			return MsgEmity.err(9001, "订单号缺失");
		}
		
		sOrderNo = sOrderNo.trim();
		
		Map<String, String> reqData = new HashMap<String, String>(4);
		reqData.put("appid", appId);
		reqData.put("mch_id", mchId);
		reqData.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); // 随机字符串，长度要求在32位以内。
		reqData.put("out_trade_no", sOrderNo);

		WXPay wxPay = new WXPay(new WXPayConfigCustom(appId, mchId, mchKey));// 创建sdk客户端
		Map<String, String> map = null;
		try {
			map = wxPay.orderQuery(reqData);
		} catch (Exception e) {
			Log.error("查询账单发生异常:", e.getMessage());
			return MsgEmity.err(9002, String.format("查询账单发生异常:%s", e.getMessage()));
		}
			
//		返回结果: {"transaction_id":"4200001184202109030102014873",
//		"nonce_str":"LGl7TvvhYdzczg3k","trade_state":"SUCCESS",
//		"bank_type":"OTHERS","openid":"opk_66kKbtasRXX7IiDgL23KOD4Y",
//		"sign":"0106EC9F44F5AF6AE297AFEA6333617D","return_msg":"OK",
//		"fee_type":"CNY","mch_id":"1612988251","cash_fee":"1",
//		"out_trade_no":"10000001","cash_fee_type":"CNY",
//		"appid":"wxc47d0dccec3c0d8e","total_fee":"1",
//		"trade_state_desc":"支付成功","trade_type":"JSAPI",
//		"result_code":"SUCCESS","attach":"","time_end":"20210903110726",
//		"is_subscribe":"Y","return_code":"SUCCESS"}
		
		//{nonce_str=MfeF6wRD4EpuAV5j, 
		//device_info=, trade_state=NOTPAY, out_trade_no=0C4EC3A7708E4D379F4A3DB51FC3D3EB, 
		//appid=wxa89cac8ca16a7a49, 
		//total_fee=3000, trade_state_desc=订单未支付, 
		//sign=50AEC31CBA566393A71816E5BD6FC12D, 
		//return_msg=OK, result_code=SUCCESS, 
		//mch_id=1619435221, return_code=SUCCESS}
		
		//当wxPay错误时,例如{return_msg=mch_id参数格式错误, return_code=FAIL}
		if ("FAIL".equals(map.get("return_code"))) {
			return MsgEmity.err(9003, String.format("请求信息错误,调用信息:%s", JacksonShell.toJSONString(map)));
		}
		
		if (map.containsKey("err_code_des")) {
			return MsgEmity.err(9004, String.format("支付失败,返回信息:%s", JacksonShell.toJSONString(map)));
		}
		
		if (!sOrderNo.equals(map.get("out_trade_no"))) {
			return MsgEmity.err(9005, String.format("返回内容的订单号与待查询订单号不符,请求订单号:'%s',返回订单号:'%s'", sOrderNo, map.get("out_trade_no")));
		}
		
		if (!"SUCCESS".equals(map.get("return_code"))) {
			return MsgEmity.err(9006, "查询交易情况失败(腾讯)");
		}
		
		if (!"SUCCESS".equals(map.get("result_code"))) {
			return MsgEmity.err(9007, "交易业务结果失败(腾讯)");
		}
		
		if (!"SUCCESS".equals(map.get("trade_state"))) {
			return MsgEmity.err(9008, "腾讯回应付款情况:", map.get("trade_state_desc"));
		}
		
		String sDate = map.get("time_end");// 20210903110726
		if (null != sDate && "".equals(sDate.trim())) {
			sDate = String.format("%s-%s-%s %s:%s:%s",
					sDate.substring(0, 4),
					sDate.substring(4, 6),
					sDate.substring(6, 8),
					sDate.substring(8, 10),
					sDate.substring(10, 12),
					sDate.substring(12));
		} else {
			sDate = "无";
		}
		
		Map<String, String> result = new HashMap<>(4);
		result.put("sOrderNo", map.get("out_trade_no"));
		result.put("sTransactionId", map.get("transaction_id"));
		result.put("iTotalFee", (new DecimalFormat("0.00")).format(Float.valueOf(map.getOrDefault("total_fee", "0")) / 100));//支付金额(元)
		result.put("queryInfo", JacksonShell.toJSONString(map));
		result.put("sState", map.get("trade_state_desc"));
		result.put("dPayTime", sDate);
		
		return MsgEmity.success(result, "订单查询成功");
	}
	
}
